﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Input;

// The Templated Control item template is documented at http://go.microsoft.com/fwlink/?LinkId=234235

namespace QKit
{
    /// <summary>
    /// Represents the method that will handle a ValueChangedComplete event.
    /// </summary>
    /// <param name="sender">The object where the handler is attached.</param>
    /// <param name="e">Event data for the event.</param>
    public delegate void ValueChangeCompletedEventHandler(object sender, RoutedEventArgs e);

    /// <summary>
    /// Represents a control allows selecting a number in a potentially large range through value stepping or gestures.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Always)]
    [TemplatePart(Name = PartDecrementButtonName, Type = typeof(RepeatButton))]
    [TemplatePart(Name = PartIncrementButtonName, Type = typeof(RepeatButton))]
    [TemplatePart(Name = PartDialMarkersName, Type = typeof(ItemsControl))]
    [TemplatePart(Name = PartDialScrollViewerName, Type = typeof(ScrollViewer))]
    public sealed class LinearDial : RangeBase
    {
        #region DependencyProperties
        /// <summary>
        /// Identifies the Header dependency property.
        /// </summary>
        public static readonly DependencyProperty HeaderProperty =
            DependencyProperty.Register("Header", typeof(object),
            typeof(LinearDial), new PropertyMetadata(null, OnHeaderChanged));

        /// <summary>
        /// Identifies the HeaderTemplate dependency property.
        /// </summary>
        public static readonly DependencyProperty HeaderTemplateProperty =
            DependencyProperty.Register("HeaderTemplate", typeof(DataTemplate),
            typeof(LinearDial), new PropertyMetadata(null));

        /// <summary>
        /// Identifies the IsLoopingEnabled dependency property.
        /// </summary>
        public static readonly DependencyProperty IsLoopingEnabledProperty =
            DependencyProperty.Register("IsLoopingEnabled", typeof(bool),
            typeof(LinearDial), new PropertyMetadata(false, OnDialLayoutPropertyChanged));

        /// <summary>
        /// Identifies the MarkerInterval dependency property.
        /// </summary>
        public static readonly DependencyProperty MarkerIntervalProperty =
            DependencyProperty.Register("MarkerInterval", typeof(double),
            typeof(LinearDial), new PropertyMetadata(5.0, OnDialLayoutPropertyChanged));

        /// <summary>
        /// Identifies the DecimalDigits dependency property.
        /// </summary>
        public static readonly DependencyProperty DecimalDigitsProperty =
            DependencyProperty.Register("DecimalDigits", typeof(int),
            typeof(LinearDial), new PropertyMetadata(0, OnDialDisplayPropertyChanged));

        /// <summary>
        /// Identifies the DisplayValue dependency property.
        /// </summary>
        public static readonly DependencyProperty DisplayValueProperty =
            DependencyProperty.Register("DisplayValue", typeof(string),
            typeof(LinearDial), new PropertyMetadata(null));
        #endregion

        #region Events
        /// <summary>
        /// Occurs when a LinearDial's value change comes to a stop after gesture events.
        /// </summary>
        public event ValueChangeCompletedEventHandler ValueChangeCompleted;
        #endregion

        #region Fields
        private const string HeaderContentPresenterName = "HeaderContentPresenter";
        private const string PartDecrementButtonName = "part_DecrementButton";
        private const string PartIncrementButtonName = "part_IncrementButton";
        private const string PartDialScrollViewerName = "part_DialScrollViewer";
        private const string PartDialMarkersName = "part_DialMarkers";
        private const double markerSpacing = 57.5;

        private ContentPresenter headerContentPresenter;
        private RepeatButton partDecrementButton;
        private RepeatButton partIncrementButton;
        private ItemsControl partDialMarkers;
        private ScrollViewer partDialScrollViewer;

        private double manipulationStartValue = 0;
        private double previousValueChangeCompletedValue = 0;
        internal double dialMarkerRange = 0;
        internal List<double> actualDialMarkerValues = new List<double>();
        internal ObservableCollection<double> displayDialMarkerValues = new ObservableCollection<double>();

        private bool initialLayoutCreated = false;
        private bool cacheIsLoopingEnabled = false;
        private double cacheMarkerInterval = 0;
        private double cacheMin = 0;
        private double cacheMax = 0;
        private double cacheWidth = 0;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the LinearDial class.
        /// </summary>
        public LinearDial()
        {
            this.DefaultStyleKey = typeof(LinearDial);
            UpdateDialDisplay(disableAnimation: true);
            this.Loaded += LinearDial_Loaded;
            this.Unloaded += LinearDial_Unloaded;
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the content for the control's header.
        /// </summary>
        public object Header
        {
            get { return GetValue(HeaderProperty); }
            set { SetValue(HeaderProperty, value); }
        }

        /// <summary>
        /// Gets or sets the DataTemplate used to display the content of the control's header.
        /// </summary>
        public DataTemplate HeaderTemplate
        {
            get { return (DataTemplate)GetValue(HeaderTemplateProperty); }
            set { SetValue(HeaderTemplateProperty, value); }
        }

        /// <summary>
        /// Gets or sets whether the dial and value should loop around when it exceeds minimum or maximum values.
        /// </summary>
        public bool IsLoopingEnabled
        {
            get { return (bool)GetValue(IsLoopingEnabledProperty); }
            set { SetValue(IsLoopingEnabledProperty, value); }
        }

        /// <summary>
        /// Gets or sets the amount the dial will skip between each dial marker value.
        /// </summary>
        public double MarkerInterval
        {
            get { return (double)GetValue(MarkerIntervalProperty); }
            set { SetValue(MarkerIntervalProperty, value); }
        }

        /// <summary>
        /// Gets or sets the number of decimal digits that will be shown when the current Value is formatted.
        /// </summary>
        public int DecimalDigits
        {
            get { return (int)GetValue(DecimalDigitsProperty); }
            set { SetValue(DecimalDigitsProperty, value); }
        }

        /// <summary>
        /// A formatted display version of the current Value.
        /// </summary>
        public string DisplayValue
        {
            get { return (string)GetValue(DisplayValueProperty); }
            private set { SetValue(DisplayValueProperty, value); }
        }
        #endregion

        #region Methods
        private void OnValueChangeCompleted(RoutedEventArgs e)
        {
            ValueChangeCompletedEventHandler handler = ValueChangeCompleted;

            if (handler != null && previousValueChangeCompletedValue != Value)
            {
                handler(this, e);
                previousValueChangeCompletedValue = Value;
            }
        }

        /// <summary>
        /// Finds the parts in the control template and hook up events and adjust layout properties.
        /// </summary>
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            headerContentPresenter = GetTemplateChild(HeaderContentPresenterName) as ContentPresenter;
            UpdateHeaderVisibility();

            if (partDecrementButton != null)
            {
                partDecrementButton.Click -= DecrementButton_Click;
                partDecrementButton.PointerCaptureLost -= Button_PointerCaptureLost;
            }
            partDecrementButton = GetTemplateChild(PartDecrementButtonName) as RepeatButton;
            if (partDecrementButton != null)
            {
                partDecrementButton.Click += DecrementButton_Click;
                partDecrementButton.PointerCaptureLost += Button_PointerCaptureLost;
            }

            if (partIncrementButton != null)
            {
                partIncrementButton.Click -= IncrementButton_Click;
                partIncrementButton.PointerCaptureLost -= Button_PointerCaptureLost;
            }
            partIncrementButton = GetTemplateChild(PartIncrementButtonName) as RepeatButton;
            if (partIncrementButton != null)
            {
                partIncrementButton.Click += IncrementButton_Click;
                partIncrementButton.PointerCaptureLost += Button_PointerCaptureLost;
            }

            partDialMarkers = GetTemplateChild(PartDialMarkersName) as ItemsControl;

            if (partDialScrollViewer != null)
                partDialScrollViewer.Loaded += PartDialScrollViewer_Loaded;
            partDialScrollViewer = GetTemplateChild(PartDialScrollViewerName) as ScrollViewer;
            if (partDialScrollViewer != null)
                partDialScrollViewer.Loaded += PartDialScrollViewer_Loaded;

            UpdateDialDisplay(disableAnimation: true);
        }

        /// <summary>
        /// Called when the Maximum property changes.
        /// </summary>
        /// <param name="oldValue">Old value of the Maximum property.</param>
        /// <param name="newValue">New value of the Maximum property.</param>
        protected override void OnValueChanged(double oldValue, double newValue)
        {
            base.OnValueChanged(oldValue, newValue);

            double highValue = Math.Max(oldValue, newValue);
            double lowValue = Math.Min(oldValue, newValue);
            bool isLoopingValueChange = (highValue - lowValue) > (Maximum - highValue) + (lowValue - Minimum);
            if (IsLoopingEnabled && partDialScrollViewer != null && isLoopingValueChange && newValue == highValue)
                UpdateDialDisplay(partDialScrollViewer.ScrollableWidth);
            else if (IsLoopingEnabled && partDialScrollViewer != null && isLoopingValueChange && newValue == lowValue)
            {
                double flowover = dialMarkerRange % 1;
                UpdateDialDisplay(markerSpacing - (partDialScrollViewer.ScrollableWidth - partDialScrollViewer.HorizontalOffset));
            }
            else
                UpdateDialDisplay();
        }

        /// <summary>
        /// Occurs when the range value changes.
        /// </summary>
        /// <param name="oldValue">Old value of the Value property.</param>
        /// <param name="newValue">New value of the Value property.</param>
        protected override void OnMaximumChanged(double oldMaximum, double newMaximum)
        {
            base.OnMaximumChanged(oldMaximum, newMaximum);

            if (oldMaximum != newMaximum)
                CreateDialMarkerLayout();
        }

        /// <summary>
        /// Called when the Minimum property changes.
        /// </summary>
        /// <param name="oldMinimum">Old value of the Minimum property.</param>
        /// <param name="newMinimum">New value of the Minimum property.</param>
        protected override void OnMinimumChanged(double oldMinimum, double newMinimum)
        {
            base.OnMinimumChanged(oldMinimum, newMinimum);

            if (oldMinimum != newMinimum)
                CreateDialMarkerLayout();
        }

        internal double NormalizeValue(double value)
        {
            double normalizedValue = value;
            double range = Maximum - Minimum;

            if (normalizedValue < Minimum)
            {
                if (IsLoopingEnabled)
                    normalizedValue = ((normalizedValue - Minimum) % range) + Maximum;
                else
                    normalizedValue = Math.Max(Minimum, normalizedValue);
            }

            if (normalizedValue >= Maximum)
            {
                if (IsLoopingEnabled)
                    normalizedValue = ((normalizedValue - Maximum) % range) + Minimum;
                else
                    normalizedValue = Math.Min(Maximum, normalizedValue);
            }

            return normalizedValue;
        }

        private void UpdateHeaderVisibility()
        {
            string displayValue = Header as string;

            if (displayValue == null)
                displayValue = Header == null ? null : Header.ToString();

            if (!string.IsNullOrEmpty(displayValue) && headerContentPresenter != null)
                headerContentPresenter.Visibility = Visibility.Visible;
            else if (headerContentPresenter != null)
                headerContentPresenter.Visibility = Visibility.Collapsed;
        }

        internal void CreateDialMarkerValues()
        {
            actualDialMarkerValues.Clear();
            displayDialMarkerValues.Clear();

            if (MarkerInterval > 0 && MarkerInterval <= Maximum)
            {
                dialMarkerRange = (Maximum - Minimum) / MarkerInterval;
                double markerCount = Math.Ceiling(dialMarkerRange);
                for (int i = 0; i <= markerCount; i++)
                {
                    double markerValue = Minimum + (((i == markerCount && IsLoopingEnabled && i > 1) ? 0 : i) * MarkerInterval);
                    actualDialMarkerValues.Add(markerValue);
                    displayDialMarkerValues.Add(markerValue);
                }
            }
        }

        internal List<double> GetRightLoopingDialValues(int loopItemPaddingCount, List<double> dialMarkerValues)
        {
            var validDialMarkerValues = dialMarkerValues.Skip(1);
            var rightLoopingDialValues = validDialMarkerValues.ToList();
            while (rightLoopingDialValues.Count < loopItemPaddingCount)
                rightLoopingDialValues.AddRange(validDialMarkerValues);
            return rightLoopingDialValues.Take(loopItemPaddingCount).ToList();
        }

        internal List<double> GetLeftLoopingDialValues(int loopItemPaddingCount, List<double> dialMarkerValues)
        {
            var validDialMarkerValues = dialMarkerValues.Take(dialMarkerValues.Count - 1).Reverse();
            var leftLoopingDialValues = validDialMarkerValues.ToList();
            while (leftLoopingDialValues.Count < loopItemPaddingCount)
                leftLoopingDialValues.AddRange(validDialMarkerValues);
            return leftLoopingDialValues.Take(loopItemPaddingCount).ToList();
        }

        private void CreateDialMarkerLayout()
        {
            if (!initialLayoutCreated ||
                cacheIsLoopingEnabled != IsLoopingEnabled ||
                cacheMarkerInterval != MarkerInterval ||
                cacheMin != Minimum || cacheMax != Maximum ||
                cacheWidth != this.ActualWidth)
            {
                if (partDialScrollViewer != null && partDialMarkers != null)
                    initialLayoutCreated = true;
                else
                    initialLayoutCreated = false;

                cacheIsLoopingEnabled = IsLoopingEnabled;
                cacheMarkerInterval = MarkerInterval;
                cacheMin = Minimum;
                cacheMax = Maximum;
                cacheWidth = ActualWidth;

                // Create dial marker values based on interval and apply style to make them uniformly spaced.
                CreateDialMarkerValues();
                ApplyItemContainerStyle();

                if (partDialScrollViewer != null)
                {
                    // To position start and end of scroll offset at center of first and last actual dial markers,
                    // we apply margins of half of the viewable area and half of the marker's width (for centering).
                    double horizontalMargin = (partDialScrollViewer.ViewportWidth - markerSpacing) / 2;
                    int loopItemPaddingCount = (int)(partDialScrollViewer.ViewportWidth / markerSpacing);

                    if (IsLoopingEnabled && actualDialMarkerValues.Count > 3 && loopItemPaddingCount > 0)
                    {
                        // Get the dial marker values left and right of actual dial markers to simulate looping.
                        var leftLoopingDialValues = GetLeftLoopingDialValues(loopItemPaddingCount, actualDialMarkerValues);
                        var rightLoopingDialValues = GetRightLoopingDialValues(loopItemPaddingCount, actualDialMarkerValues);

                        // Additional adjustments for the margins to account for new left and right looping values.
                        horizontalMargin -= markerSpacing * loopItemPaddingCount;

                        // Actually add those looping dial marker values (note left ones are purposedly
                        // returned reversed so we insert at index 0 to unreverse them).
                        for (int i = 0; i < loopItemPaddingCount; i++)
                        {
                            displayDialMarkerValues.Insert(0, leftLoopingDialValues[i]);
                            displayDialMarkerValues.Add(rightLoopingDialValues[i]);
                        }
                    }

                    // Apply margin calculated from above. When looping is enabled, add aditional
                    // MarkerSpacing on the left so we can use that for smooth scrolling from 
                    // Maximum value looping back to Minimum value.
                    partDialMarkers.Margin = new Thickness(horizontalMargin + (IsLoopingEnabled ? markerSpacing : 0), 0, horizontalMargin, 0);
                }

                if (partDialMarkers != null)
                    partDialMarkers.ItemsSource = displayDialMarkerValues;
            }

            UpdateDialDisplay(disableAnimation: true);
        }

        private void ApplyItemContainerStyle()
        {
            if (partDialMarkers != null)
            {
                var style = new Style(typeof(FrameworkElement));
                style.Setters.Add(new Setter(FrameworkElement.WidthProperty, markerSpacing));
                partDialMarkers.ItemContainerStyle = style;
            }
        }

        private void UpdateDialDisplay(double? animateFromOffset = null, bool disableAnimation = false)
        {
            DisplayValue = Math.Round(Value, DecimalDigits).ToString("F" + DecimalDigits);

            // Scroll to correct dial marker position
            if (partDialScrollViewer != null)
            {
                partDialScrollViewer.ChangeView(animateFromOffset, null, null, true);
                partDialScrollViewer.ChangeView(GetDialScrollOffset(Value), null, null, disableAnimation);
            }
        }

        internal double GetDialScrollOffset(double value)
        {
            double percentValue = Maximum == Minimum ? 0 : (value - Minimum) / (Maximum - Minimum);
            double availableScrollOffset = dialMarkerRange * markerSpacing;

            // Returned calculated scroll offset. Also account for the extra MarkerSpacing 
            // to the left of the dial when looping is enabled.
            return (availableScrollOffset * percentValue) + (IsLoopingEnabled ? markerSpacing : 0);
        }
        #endregion

        #region Event Handlers
        private static void OnHeaderChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var linearDial = dependencyObject as LinearDial;
            if (linearDial != null)
                linearDial.UpdateHeaderVisibility();
        }

        private static void OnDialLayoutPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var linearDial = dependencyObject as LinearDial;
            if (linearDial != null)
                linearDial.CreateDialMarkerLayout();
        }

        private static void OnDialDisplayPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var linearDial = dependencyObject as LinearDial;
            if (linearDial != null)
                linearDial.UpdateDialDisplay();
        }

        private void LinearDial_Loaded(object sender, RoutedEventArgs e)
        {
            CreateDialMarkerLayout();
            this.SizeChanged += LinearDial_SizeChanged;
            this.ManipulationStarted += LinearDial_ManipulationStarted;
            this.ManipulationDelta += LinearDial_ManipulationDelta;
            this.ManipulationCompleted += LinearDial_ManipulationCompleted;
        }

        private void LinearDial_Unloaded(object sender, RoutedEventArgs e)
        {
            this.SizeChanged -= LinearDial_SizeChanged;
            this.ManipulationStarted -= LinearDial_ManipulationStarted;
            this.ManipulationDelta -= LinearDial_ManipulationDelta;
            this.ManipulationCompleted -= LinearDial_ManipulationCompleted;
        }

        private void DecrementButton_Click(object sender, RoutedEventArgs e)
        {
            double newValue = Value - SmallChange;
            Value = NormalizeValue(newValue);
        }

        private void IncrementButton_Click(object sender, RoutedEventArgs e)
        {
            double newValue = Value + SmallChange;
            Value = NormalizeValue(newValue);
        }

        private void Button_PointerCaptureLost(object sender, PointerRoutedEventArgs e)
        {
            OnValueChangeCompleted(new RoutedEventArgs());
        }

        private void LinearDial_ManipulationStarted(object sender, ManipulationStartedRoutedEventArgs e)
        {
            manipulationStartValue = Value;
        }

        private void LinearDial_ManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
        {
            double valueDelta = LargeChange * (e.Cumulative.Translation.X / 75);
            valueDelta -= valueDelta % SmallChange;
            double newValue = manipulationStartValue - valueDelta;
            Value = NormalizeValue(newValue);

            if (!IsLoopingEnabled && e.IsInertial && (Value == Minimum || Value == Maximum))
                e.Complete();
        }

        private void LinearDial_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
        {
            OnValueChangeCompleted(new RoutedEventArgs());
        }

        private void LinearDial_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (e.PreviousSize != e.NewSize)
                CreateDialMarkerLayout();
        }

        private void PartDialScrollViewer_Loaded(object sender, RoutedEventArgs e)
        {
            partDialScrollViewer.UpdateLayout();
            UpdateDialDisplay(disableAnimation: true);
        }
        #endregion
    }
}